﻿Imports System.Runtime.CompilerServices

''' <summary>
''' Предоставляет методы расширения для визуализации элементов навигации. Совместимо с Twitter Bootstrap.
''' </summary>
''' <remarks>При визулизации элементов навигации используется HTML-разметка и наименования CSS-классов для совместного использования с фрейморком Twitter Bootstrap.</remarks>
Public Module NavigationHtmlHelper
    Const dropdownClassName As String = "dropdown"
    Const dropdownToggleClassName As String = "dropdown-toggle"
    Const dropdownDataToggleName As String = "dropdown"
    Const caretClassName As String = "caret"
    Const dividerClassName As String = "divider"
    Const dropdownMenuClassName As String = "dropdown-menu"
    Const expandedClassName As String = "expanded"
    Const activeClassName As String = "active"

    'Принимает значение из пакета представления ViewBag.ParentUrl, переданного через экземпляр вспомогательного метода HTML.
    Private _parentUrl As String

#Region " Навигация "

    ''' <summary>
    ''' Предоставляет разметку для немаркированного списка, отображающего навигационную структуру сайта.
    ''' </summary>
    ''' <param name="helper">Экземпляр вспомогательного метода HTML, который расширяется данным методом.</param>
    ''' <returns>Объект <see cref="MvcHtmlString" />.</returns>
    ''' <remarks></remarks>
    <Extension()>
    Public Function Navigation(helper As HtmlHelper) As MvcHtmlString
        Return Navigation(helper, htmlAttributes:=Nothing)
    End Function

    ''' <summary>
    ''' Предоставляет разметку для немаркированного списка, отображающего навигационную структуру сайта.
    ''' </summary>
    ''' <param name="helper">Экземпляр вспомогательного метода HTML, который расширяется данным методом.</param>
    ''' <param name="hideRootNode">Разрешает скрывать корневой узел.</param>
    ''' <returns>Объект <see cref="MvcHtmlString" />.</returns>
    ''' <remarks></remarks>
    <Extension()>
    Public Function Navigation(helper As HtmlHelper, hideRootNode As Boolean) As MvcHtmlString
        Return Navigation(helper, hideRootNode:=hideRootNode, htmlAttributes:=Nothing)
    End Function

    ''' <summary>
    ''' Предоставляет разметку для немаркированного списка, отображающего навигационную структуру сайта.
    ''' </summary>
    ''' <param name="helper">Экземпляр вспомогательного метода HTML, который расширяется данным методом.</param>
    ''' <param name="htmlAttributes">Объект, который содержит HTML-аттрибуты для элемента.</param>
    ''' <returns>Объект <see cref="MvcHtmlString" />.</returns>
    ''' <remarks></remarks>
    <Extension()>
    Public Function Navigation(helper As HtmlHelper, htmlAttributes As Object) As MvcHtmlString
        Return Navigation(helper, hideRootNode:=False, htmlAttributes:=htmlAttributes)
    End Function

    ''' <summary>
    ''' Предоставляет разметку для немаркированного списка, отображающего навигационную структуру сайта.
    ''' </summary>
    ''' <param name="helper">Экземпляр вспомогательного метода HTML, который расширяется данным методом.</param>
    ''' <param name="hideRootNode">Разрешает скрывать корневой узел.</param>
    ''' <param name="htmlAttributes">Объект, который содержит HTML-аттрибуты для элемента.</param>
    ''' <returns>Объект <see cref="MvcHtmlString" />.</returns>
    ''' <remarks></remarks>
    <Extension()>
    Public Function Navigation(helper As HtmlHelper, hideRootNode As Boolean, htmlAttributes As Object) As MvcHtmlString
        Return Navigation(helper, 0, hideRootNode, htmlAttributes)
    End Function

    ''' <summary>
    ''' Предоставляет разметку для немаркированного списка, отображающего навигационную структуру сайта.
    ''' </summary>
    ''' <param name="helper">Экземпляр вспомогательного метода HTML, который расширяется данным методом.</param>
    ''' <param name="childLevels">Количество дочерних уровней.</param>
    ''' <returns>Объект <see cref="MvcHtmlString" />.</returns>
    ''' <remarks></remarks>
    <Extension()>
    Public Function Navigation(helper As HtmlHelper, childLevels As Integer) As MvcHtmlString
        Return Navigation(helper, childLevels, False)
    End Function

    ''' <summary>
    ''' Предоставляет разметку для немаркированного списка, отображающего навигационную структуру сайта.
    ''' </summary>
    ''' <param name="helper">Экземпляр вспомогательного метода HTML, который расширяется данным методом.</param>
    ''' <param name="childLevels">Количество дочерних уровней.</param>
    ''' <param name="hideRootNode">Разрешает скрывать стартовый узел.</param>
    ''' <returns>Объект <see cref="MvcHtmlString" />.</returns>
    ''' <remarks></remarks>
    <Extension()>
    Public Function Navigation(helper As HtmlHelper, childLevels As Integer, hideRootNode As Boolean) As MvcHtmlString
        Return Navigation(helper, childLevels, hideRootNode, Nothing)
    End Function

    ''' <summary>
    ''' Предоставляет разметку для немаркированного списка, отображающего навигационную структуру сайта.
    ''' </summary>
    ''' <param name="helper">Экземпляр вспомогательного метода HTML, который расширяется данным методом.</param>
    ''' <param name="childLevels">Количество дочерних уровней.</param>
    ''' <param name="htmlAttributes">Объект, который содержит HTML-аттрибуты для элемента.</param>
    ''' <returns>Объект <see cref="MvcHtmlString" />.</returns>
    ''' <remarks></remarks>
    <Extension()>
    Public Function Navigation(helper As HtmlHelper, childLevels As Integer, htmlAttributes As Object) As MvcHtmlString
        Return Navigation(helper, childLevels, False, htmlAttributes)
    End Function

    ''' <summary>
    ''' Предоставляет разметку для немаркированного списка, отображающего навигационную структуру сайта.
    ''' </summary>
    ''' <param name="helper">Экземпляр вспомогательного метода HTML, который расширяется данным методом.</param>
    ''' <param name="childLevels">Количество дочерних уровней.</param>
    ''' <param name="hideRootNode">Разрешает скрывать стартовый узел.</param>
    ''' <param name="htmlAttributes">Объект, который содержит HTML-аттрибуты для элемента.</param>
    ''' <returns>Объект <see cref="MvcHtmlString" />.</returns>
    ''' <remarks></remarks>
    <Extension()>
    Public Function Navigation(helper As HtmlHelper, childLevels As Integer, hideRootNode As Boolean, htmlAttributes As Object) As MvcHtmlString
        Return Navigation(helper, SiteMap.RootNode, childLevels, hideRootNode, htmlAttributes)
    End Function

    ''' <summary>
    ''' Предоставляет разметку для немаркированного списка, отображающего навигационную структуру сайта, скрывая узлы, переданные через параметр <paramref name="excludedUrl" />.
    ''' </summary>
    ''' <param name="helper">Экземпляр вспомогательного метода HTML, который расширяется данным методом.</param>
    ''' <param name="hideRootNode">Разрешает скрывать стартовый узел.</param>
    ''' <param name="htmlAttributes">Объект, который содержит HTML-аттрибуты для элемента.</param>
    ''' <returns>Объект <see cref="MvcHtmlString" />.</returns>
    ''' <remarks></remarks>
    <Extension()>
    Public Function Navigation(helper As HtmlHelper, hideRootNode As Boolean, htmlAttributes As Object, ParamArray excludedUrls() As String) As MvcHtmlString
        _parentUrl = helper.ViewBag.ParentUrl
        Return New MvcHtmlString(BuildUl(SiteMap.RootNode, 0, hideRootNode, htmlAttributes, excludedUrls))
    End Function

    ''' <summary>
    ''' Предоставляет разметку для немаркированного списка, отображающего навигационную структуру сайта, скрывая узлы, переданные через параметр <paramref name="excludedUrl" />.
    ''' </summary>
    ''' <param name="helper">Экземпляр вспомогательного метода HTML, который расширяется данным методом.</param>
    ''' <param name="childLevels">Количество дочерних уровней.</param>
    ''' <param name="hideRootNode">Разрешает скрывать стартовый узел.</param>
    ''' <param name="htmlAttributes">Объект, который содержит HTML-аттрибуты для элемента.</param>
    ''' <returns>Объект <see cref="MvcHtmlString" />.</returns>
    ''' <remarks></remarks>
    <Extension()>
    Public Function Navigation(helper As HtmlHelper, childLevels As Integer, hideRootNode As Boolean, htmlAttributes As Object, ParamArray excludedUrls() As String) As MvcHtmlString
        _parentUrl = helper.ViewBag.ParentUrl
        Return New MvcHtmlString(BuildUl(SiteMap.RootNode, childLevels, hideRootNode, htmlAttributes, excludedUrls))
    End Function

    ''' <summary>
    ''' Предоставляет разметку для немаркированного списка, отображающего навигационную структуру сайта.
    ''' </summary>
    ''' <param name="helper">Экземпляр вспомогательного метода HTML, который расширяется данным методом.</param>
    ''' <param name="startNode">Узел карты сайта, относительно которого будет формироваться разметка меню.</param>
    ''' <param name="childLevels">Количество дочерних уровней.</param>
    ''' <param name="hideRootNode">Разрешает скрывать стартовый узел.</param>
    ''' <param name="htmlAttributes">Объект, который содержит HTML-аттрибуты для элемента.</param>
    ''' <returns>Объект <see cref="MvcHtmlString" />.</returns>
    ''' <remarks></remarks>
    <Extension()>
    Public Function Navigation(helper As HtmlHelper, startNode As SiteMapNode, childLevels As Integer, hideRootNode As Boolean, htmlAttributes As Object) As MvcHtmlString
        _parentUrl = helper.ViewBag.ParentUrl
        Return New MvcHtmlString(BuildUl(startNode, childLevels, hideRootNode, htmlAttributes))
    End Function

    Private Function BuildUl(node As SiteMapNode, childLevels As Integer, hideRootNode As Boolean, htmlAttributes As Object, ParamArray excludedUrls() As String) As String
        If node Is Nothing Then node = SiteMap.RootNode

        Dim ulTag As New TagBuilder("ul")

        With ulTag
            .MergeAttributes(New RouteValueDictionary(htmlAttributes), True)

            If Not hideRootNode Then .InnerHtml = BuildLi(node, 0)

            For index = 0 To node.ChildNodes.Count - 1
                Dim liString As String = BuildLi(node.ChildNodes.Item(index), childLevels, excludedUrls)

                .InnerHtml += liString

                'Если пункт меню является выделенным, т.е. в его строке кода содержится слово 'active', то самому меню добавляем класс стиля 'expanded'.
                If liString.Contains(activeClassName) Then .AddCssClass(expandedClassName)
            Next
        End With

        Return ulTag.ToString(TagRenderMode.Normal)
    End Function

    Private Function BuildLi(node As SiteMapNode, childLevels As Integer, ParamArray excludedUrls() As String) As String
        If excludedUrls.Contains(node.Url) Then Return String.Empty

        Dim liTag As New TagBuilder("li")
        With liTag
            'Если узел является активным или его Url совпадает с родительским Url, то пометим его соответствующим классом.
            If node.Equals(SiteMap.CurrentNode) Or node.Url = _parentUrl Then .AddCssClass(activeClassName)

            If node.HasChildNodes And childLevels <> 0 Then ' Если узел имеет дочерние узлы, то рекурсивно вызовим функцию BuildUl.
                .AddCssClass(dropdownClassName)
                liTag.InnerHtml += BuildA(node.Title, "#", dropdownToggleClassName, dropdownDataToggleName, String.Empty).ToString(TagRenderMode.Normal)
                .InnerHtml += BuildUl(node, childLevels - 1, False, New With {.class = dropdownMenuClassName}, excludedUrls)
            ElseIf Not String.IsNullOrEmpty(node.Url) Then ' Если узел имеет Url, то добавим ссылку.
                liTag.InnerHtml += BuildA(node.Title, VirtualPathUtility.ToAbsolute(node.Url), String.Empty, String.Empty, String.Empty).ToString(TagRenderMode.Normal)
            End If
        End With
        Return liTag.ToString(TagRenderMode.Normal)
    End Function

#End Region

#Region " Хлебные крошки "

    ''' <summary>
    ''' Предоставляет разметку навигации типа "хлебные крошки".
    ''' </summary>
    ''' <param name="helper">Экземпляр вспомогательного метода HTML, который расширяется данным методом.</param>
    ''' <returns>Объект <see cref="MvcHtmlString" />.</returns>
    ''' <remarks></remarks>
    <Extension()>
    Public Function Breadcrumb(helper As HtmlHelper) As MvcHtmlString
        Return Breadcrumb(helper, Nothing)
    End Function

    ''' <summary>
    ''' Предоставляет разметку навигации типа "хлебные крошки".
    ''' </summary>
    ''' <param name="helper">Экземпляр вспомогательного метода HTML, который расширяется данным методом.</param>
    ''' <param name="htmlAttributes">Объект, который содержит HTML-аттрибуты для элемента.</param>
    ''' <returns>Объект <see cref="MvcHtmlString" />.</returns>
    ''' <remarks></remarks>
    <Extension()>
    Public Function Breadcrumb(helper As HtmlHelper, htmlAttributes As Object) As MvcHtmlString
        Return Breadcrumb(helper, "►", htmlAttributes)
    End Function

    ''' <summary>
    ''' Предоставляет разметку навигации типа "хлебные крошки".
    ''' </summary>
    ''' <param name="helper">Экземпляр вспомогательного метода HTML, который расширяется данным методом.</param>
    ''' <param name="divider">Строка, содержащая знак разделителя.</param>
    ''' <param name="htmlAttributes">Объект, который содержит HTML-аттрибуты для элемента.</param>
    ''' <returns>Объект <see cref="MvcHtmlString" />.</returns>
    ''' <remarks></remarks>
    <Extension()>
    Public Function Breadcrumb(helper As HtmlHelper, divider As String, htmlAttributes As Object) As MvcHtmlString
        _parentUrl = helper.ViewBag.ParentUrl
        Dim node = SiteMap.CurrentNode

        If node IsNot Nothing AndAlso node Is SiteMap.RootNode Then
            Return New MvcHtmlString(String.Empty)
        ElseIf node Is Nothing And Not String.IsNullOrEmpty(_parentUrl) Then
            node = SiteMap.Provider.FindSiteMapNode(_parentUrl)
        End If

        If node Is Nothing Then
            node = SiteMap.RootNode
            divider = String.Empty
        End If

        Dim ulTag As New TagBuilder("ul")
        With ulTag
            .MergeAttributes(New RouteValueDictionary(htmlAttributes))
            .InnerHtml = BuildBreadcrumb(ulTag, node, divider)
        End With

        Return MvcHtmlString.Create(ulTag.ToString(TagRenderMode.Normal))
    End Function

    Private Function BuildBreadcrumb(ul As TagBuilder, node As SiteMapNode, divider As String) As String
        If Not String.IsNullOrEmpty(node.Url) Then
            ul.InnerHtml = BuildListItem(node, divider) & ul.InnerHtml
        End If
        'Если узел имеет родительский узел, то повторно вызывается функция BuildBreadcrumb.
        If node.ParentNode IsNot Nothing Then
            BuildBreadcrumb(ul, node.ParentNode, divider)
        End If
        Return ul.InnerHtml
    End Function

    Private Function BuildListItem(node As SiteMapNode, divider As String) As String
        Dim liTag As New TagBuilder("li")
        With liTag
            If node.Equals(SiteMap.CurrentNode) Then
                .AddCssClass(activeClassName)
                .InnerHtml = node.Title
            Else
                .InnerHtml = BuildBreadcrumbAnchor(node)
                If node.HasChildNodes Then
                    If SiteMap.CurrentNode IsNot Nothing Or Not node.Url.Equals(_parentUrl) Then
                        .InnerHtml += BuildBreadcrumbDivider(divider)
                    End If
                End If
            End If
        End With
        Return liTag.ToString(TagRenderMode.Normal)
    End Function

    Private Function BuildBreadcrumbAnchor(node As SiteMapNode) As String
        Dim aTag As New TagBuilder("a")
        With aTag
            .InnerHtml = node.Title
            If Not node.Equals(SiteMap.CurrentNode) Then
                .Attributes.Add("href", VirtualPathUtility.ToAbsolute(node.Url))
                'Если узел имеет описание, то добавим к тэгу аттрибут title.
                If Not String.IsNullOrEmpty(node.Description) Then .Attributes.Add("title", node.Description)
            End If
        End With
        Return aTag.ToString(TagRenderMode.Normal)
    End Function

    Private Function BuildBreadcrumbDivider(divider As String) As String
        If String.IsNullOrEmpty(divider) Then Return String.Empty

        Dim spanTag As New TagBuilder("span")
        With spanTag
            .AddCssClass(dividerClassName)
            .InnerHtml = divider
        End With

        Return spanTag.ToString(TagRenderMode.Normal)
    End Function

#End Region

    Private Function BuildA(title As String, href As String, className As String, dataToggle As String, iconClassName As String) As TagBuilder
        Dim aTag As New TagBuilder("a")
        If Not String.IsNullOrEmpty(dataToggle) Then aTag.Attributes.Add("data-toggle", dataToggle)
        If Not String.IsNullOrEmpty(className) Then aTag.AddCssClass(className)
        If Not String.IsNullOrEmpty(href) Then aTag.Attributes.Add("href", href)
        If Not String.IsNullOrEmpty(iconClassName) Then aTag.InnerHtml = BuildIcon(iconClassName).ToString(TagRenderMode.Normal) & " "
        aTag.InnerHtml += title
        If Not String.IsNullOrEmpty(dataToggle) AndAlso dataToggle.Equals(dropdownDataToggleName) Then aTag.InnerHtml += " " & BuildCaret.ToString(TagRenderMode.Normal)
        Return aTag
    End Function

    Private Function BuildCaret() As TagBuilder
        Dim bTag As New TagBuilder("b")
        bTag.AddCssClass(caretClassName)
        Return bTag
    End Function

    Private Function BuildIcon(className As String) As TagBuilder
        Dim iTag As New TagBuilder("i")
        iTag.AddCssClass(className)
        Return iTag
    End Function

End Module
